package queryable.store;
/**

 * Copyright 2011 rkehoe
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * 
 */


import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;

import org.springframework.util.ReflectionUtils;

import queryable.store.annotations.Ignore;
import queryable.store.annotations.Index;
import queryable.store.annotations.Indexes;

/**
 * <p>
 * <b>Purpose</b>: 
 * To generate metadata on domain objects to aid in the construction of data containers.
 * <p>
 * <b>Description</b>: 
 * Utility class that generates and caches metadata on domain objects
 * using reflection. Also provides basic property extraction
 * on objects that are annotated (fields and methods) with {@link Column}.
 * 
 * @author rkehoe
 */
public class EntityReflector
{
	private final Map<Class<?>, DomainObjectMetadata>	metadataCache	= new ConcurrentHashMap<Class<?>, DomainObjectMetadata>();

    public DomainObjectMetadata getDomainObjectMetadata(Class<?> klass)
    {
	    DomainObjectMetadata metadata = this.getMetadata(klass);
	    return metadata;
    }

	public Map<String, Object> getProperties(Object bean) throws Exception
	{
		if(bean==null)return Collections.emptyMap();
		DomainObjectMetadata metadata = getMetadata(bean.getClass());
    	Map<String,Object> map = metadata.extractFieldValues(bean);
		return map;
	}

	public List<Index> getIndexes(Class<?> klass)
	{
    	DomainObjectMetadata metadata = this.getMetadata(klass);
		return metadata.getIndexes();
	}
	
    public List<IFieldMetadata> getFields(Class<?> klass)
    {
    	DomainObjectMetadata metadata = this.getMetadata(klass);
    	return metadata.getQueryFields();
    }

    private static void log(String x)
    {
	    System.out.println(x);
    }

	private DomainObjectMetadata getMetadata(final Class<?> clazz)
	{
		DomainObjectMetadata metadata = this.metadataCache.get(clazz);

		if (metadata == null)
		{
			synchronized (this.metadataCache)
			{
				metadata = this.metadataCache.get(clazz);
				if (metadata == null)
				{
					log("Generating Injection Metadata for Class: "+clazz);
					final DomainObjectMetadata newMetadata = new DomainObjectMetadata(clazz);
					
					ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback()
					{
						public void doWith(Field field)
						{
							/*
							 * Check to see if field has @Ignore - if it does then ignore the field. 
							 */
							if(field.getAnnotation(Ignore.class)!=null)return;

							Column injectionAnnotation = field.getAnnotation(Column.class);
							if (injectionAnnotation != null)
							{
								if (Modifier.isStatic(field.getModifiers()))
								{
									throw new IllegalStateException("Autowired annotation is not supported on static fields");
								}
								ReflectionUtils.makeAccessible(field);
								QueryFieldMetadata fieldElement = new QueryFieldMetadata(field,injectionAnnotation);
								log("Found field '"+fieldElement+"' w/ "+Column.class.getSimpleName());
								newMetadata.addQueryField(fieldElement);
							}

							Id idAnnotation = field.getAnnotation(Id.class);
							if (idAnnotation != null)
							{
								if (Modifier.isStatic(field.getModifiers()))
								{
									throw new IllegalStateException("Autowired annotation is not supported on static fields");
								}
								ReflectionUtils.makeAccessible(field);
								IDFieldMetadata fieldElement = new IDFieldMetadata(field,idAnnotation);
								log("Found field '"+fieldElement+"' w/ "+Column.class.getSimpleName());
								newMetadata.addIDField(fieldElement);
							}
						}
					});

					ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback()
					{
						
						@Override
						public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException
						{
							/*
							 * Check to see if field has @Ignore - if it does then ignore the field. 
							 */
							if(method.getAnnotation(Ignore.class)!=null)return;

							Column injectionAnnotation = method.getAnnotation(Column.class);
							if (injectionAnnotation != null)
							{
								QueryMethodMetadata methodMD = new QueryMethodMetadata(method,injectionAnnotation);
								log("Found method '"+methodMD+"' w/ "+Column.class.getSimpleName());
								newMetadata.addQueryMethod(methodMD);
							}
						}
					});
					

					metadata = newMetadata;
					
					this.metadataCache.put(clazz, metadata);
				}
			}
		}
		return metadata;
	}

	public static class DomainObjectMetadata
	{
		private Set<QueryFieldMetadata> queryFields = new LinkedHashSet<QueryFieldMetadata>();
		private Set<QueryMethodMetadata> queryMethods = new LinkedHashSet<QueryMethodMetadata>();
		@SuppressWarnings("unused")
        private final Class<?> clazz;
		private List<Index> indexes = new ArrayList<Index>();
		private IDFieldMetadata idFieldMetadata;
		private Table table;

        public DomainObjectMetadata(Class<?> clazz)
        {
			this.clazz = clazz;
			Index index = clazz.getAnnotation(Index.class);
			if(index!=null)
			{
				this.indexes.add(index);				
			}
			Indexes indexes = clazz.getAnnotation(Indexes.class);
			if(indexes!=null)
			{
				Index[] idxs = indexes.value();
				this.indexes.addAll(Arrays.asList(idxs));				
			}
			table = clazz.getAnnotation(Table.class);
        }

        /**
         * @param fieldElement
         */
        public void addIDField(IDFieldMetadata fieldElement)
        {
			this.idFieldMetadata = fieldElement;
        	
        }

		/**
         * @param methodMD
         */
        public void addQueryMethod(QueryMethodMetadata methodMD)
        {
	        this.queryMethods.add(methodMD);
        }

		/**
         * @return the indexes
         */
        public List<Index> getIndexes()
        {
	        return this.indexes;
        }
        
        public Map<String,Object> extractFieldValues(Object bean) throws RuntimeException, Exception
        {
        	Map<String,Object> map = new LinkedHashMap<String,Object>();
	        
        	for ( QueryFieldMetadata f :this.queryFields)
            {
        		Field field = f.getField();
	            Object fieldValue = field.get(bean);
	            String fieldName = field.getName();
	            map.put(fieldName, fieldValue);
            }

        	for ( QueryMethodMetadata m :this.queryMethods)
            {
        		Method method = m.getMethod();
	            Object fieldValue = method.invoke(bean);
	            String fieldName = m.getName();
	            map.put(fieldName, fieldValue);
            }
        	
        	Object idFieldValue = this.idFieldMetadata.field.get(bean);
        	String idFieldName = this.idFieldMetadata.field.getName();
            map.put(idFieldName, idFieldValue);

        	return map;
        }

        public void addQueryField(QueryFieldMetadata queryField)
        {
        	queryFields.add(queryField);	        
        }		
        
        /**
         * TODO - rename! and trace users!
         * @return the queryFields
         */
        public List<IFieldMetadata> getQueryFields()
        {
        	List<IFieldMetadata> list = new ArrayList<IFieldMetadata>();
        	list.addAll(queryFields);
        	list.addAll(queryMethods);
        	list.add(idFieldMetadata);
	        return list;
        }

		/**
         * @return the idFieldMetadata
         */
        public IDFieldMetadata getIdFieldMetadata()
        {
	        return idFieldMetadata;
        }

		/**
         * @param idFieldMetadata the idFieldMetadata to set
         */
        public void setIdFieldMetadata(IDFieldMetadata idFieldMetadata)
        {
	        this.idFieldMetadata = idFieldMetadata;
        }
        
        /**
         * @return the table
         */
        public Table getTable()
        {
	        return this.table;
        }
	}
	
	public static class IDFieldMetadata implements IFieldMetadata
	{

		private final Field field;
		private final Id annotation;
		private String name;

		/**
         * @param field
         * @param annotation
         */
        public IDFieldMetadata(Field field, Id annotation)
        {
			this.field = field;
			this.annotation = annotation;
			name = this.field.getName();
        }

		/* (non-Javadoc)
         * @see queryable.store.IFieldMetadata#getName()
         */
        @Override
        public String getName()
        {
	        return name;
        }

		/* (non-Javadoc)
         * @see queryable.store.IFieldMetadata#getType()
         */
        @Override
        public Class<? extends Object> getType()
        {
	        return this.field.getType();
        }
		
	}
	
	public static class QueryFieldMetadata implements IFieldMetadata
	{
		private final Field field;
		private final Column annotation;
		private final String name;

        public QueryFieldMetadata(Field field, Column injectionAnnotation)
        {
			this.field = field;
			this.annotation = injectionAnnotation;        	
			if(this.annotation.name().isEmpty())
		       name = this.field.getName();
			else
				name = this.annotation.name();
        }

        public Field getField()
        {
	        return this.field;
        }

		/* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
	        return QueryFieldMetadata.class.getSimpleName()+" [annotation=" + this.annotation + ", field=" + this.field + "]";
        }

		/* (non-Javadoc)
         * @see test.store.IFieldMetadata#getName()
         */
        @Override
        public String getName()
        {
        	return this.name;
        }

		/* (non-Javadoc)
         * @see test.store.IFieldMetadata#getType()
         */
        @Override
        public Class<? extends Object> getType()
        {
	        return this.field.getType();
        }        
	}

	public static class QueryMethodMetadata implements IFieldMetadata
	{
		private final Column annotation;
		private final String name;
		private final Method method;

        public QueryMethodMetadata(Method method, Column injectionAnnotation)
        {
			this.method = method;
			this.annotation = injectionAnnotation;        	
			if(this.annotation.name().isEmpty())
		       name = deriveName(this.method.getName());
			else
				name = this.annotation.name();
        }

        private String deriveName(String name)
        {
        	if(name.startsWith("get"))
        		return name.substring(3, name.length());
        	if(name.startsWith("is"))
        		return name.substring(2, name.length());
        	if(name.startsWith("has"))
        		return name.substring(3, name.length());
	        return name;
        }

        public Method getMethod()
        {
	        return this.method;
        }

		/* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
	        return QueryFieldMetadata.class.getSimpleName()+" [annotation=" + this.annotation + ", method=" + this.method + "]";
        }

		/* (non-Javadoc)
         * @see test.store.IFieldMetadata#getName()
         */
        @Override
        public String getName()
        {
        	return this.name;
        }

		/* (non-Javadoc)
         * @see test.store.IFieldMetadata#getType()
         */
        @Override
        public Class<? extends Object> getType()
        {
	        return this.method.getReturnType();
        }
	}

    /**
     * Utility method to set the field annotated with {@link Id}
     * if that field is null.
     * @param uid
     * @param entity
     * @return 
     */
    public Object setIDifNull(IDGenerator idGen,Object entity)
    {
	    DomainObjectMetadata metadata = this.getMetadata(entity.getClass());
	    try
        {
	    	Object id = metadata.idFieldMetadata.field.get(entity);
			if(id==null)
            {
	            id = idGen.nextID();
	            metadata.idFieldMetadata.field.set(entity, id);
            }
			return id;
        }
        catch (Exception e)
        {
	        throw new RuntimeException( e );
        }
    }
}
                              